package com.springboot.java_doc_searcher.searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * Description :
 * User: jiume
 * Date: 2025-06-03
 * Time: 18:21
 */
// 通过这个类在内存中构造索引
public class Index {
    private static final String INDEX_PATH = "D:/project/java-doc-searcher/index_path/";
    // 正排索引: id -> docInfo -> 数组
    private static List<DocInfo> forwardIndex = new ArrayList<>();
    // 倒排索引: 词 -> List<id>(List<Weight>) -> 哈希表
    private static HashMap<String, List<Weight>> invertedIndex = new HashMap<>();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    // 创建两个锁对象
    private static final Object LOCKER1 = new Object();
    private static final Object LOCKER2 = new Object();

    /**
     * 1、构造正排索引: 根据文档Id获取文档信息
     * @param docId 文档Id
     * @return 文档对象
     */
    public DocInfo getDocInfo(int docId) {
        if (docId < 0) {
            return null;
        }
        return forwardIndex.get(docId);
    }

    /**
     * 2、构造倒排索引: 根据查询词获取文档Id列表
     * 虽然文档Id确实是能够表示文档，但是查询出的结果应该要有优先级才对
     * 这里使用 weight类 来表示文档与某个查询词的优先级正相关
     * @param term 分词后的查询词
     * @return 文档Id列表
     */
    public List<Weight> getInverted(String term) {
        if (!StringUtils.hasText(term)) {
            return null;
        }
        return invertedIndex.get(term);
    }

    /**
     * 3、新增文档
     * @param title 文档标题
     * @param url 文档URL
     * @param content 文档正文
     */
    public void addDoc(String title, String url, String content) {
        // 新增正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 新增倒排索引
        buildInverted(docInfo);
    }

    // 新增倒排索引
    private void buildInverted(DocInfo docInfo) {
        // 由于多处使用到invertedIndex，因此直接在最外层进行加锁操作
        synchronized (LOCKER2) {
            // 倒排索引的本质是通过词来找到文档，进而找到文档Id
            // 这个词也就是查询词，而查询词是经过了分词处理的，因此文档中蕴涵的词也需要分词处理
            // 这里的词包括title和content，分词完成之后，为了后续展示，得统计每个词出现的权重
            // 这里的权重，我们简单统计频次即可（使用HashMap），因为title和content的权重占比不同
            // 因此得分别统计，最终综合时，weight = title出现的次数 * 10 + content

            // 因为一个哈希表只能统计title或者content出现的次数，为了方便，使用内部类代表
            class WordCnt {
                // 表示该词在title和content中分别出现的次数
                int titleCnt;
                int contentCnt;
            }
            HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();
            // 1、分词处理并分别统计词频
            // 对title和content分别进行分词处理，然后统计词频
            List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
            for (Term term : terms) {
                String word = term.getName();
                // 判断该词是否在哈希表中已经存在
                if (!wordCntHashMap.containsKey(word)) {
                    // 不存在，就创建该词
                    wordCntHashMap.put(word, new WordCnt());
                }
                // 运行到这里，说明词一定存在了。接下来就是统计title+1
                wordCntHashMap.get(word).titleCnt += 1;
            }
            terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
            for (Term term : terms) {
                String word = term.getName();
                // 判断该词是否在哈希表中已经存在
                if (!wordCntHashMap.containsKey(word)) {
                    // 不存在，就创建该词
                    wordCntHashMap.put(word, new WordCnt());
                }
                // 运行到这里，说明词一定存在了。接下来就是统计content+1
                wordCntHashMap.get(word).contentCnt += 1;
            }
            // 2、综合词频到总的哈希表中: 遍历当前文档的词频哈希表，
            for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
                // 判断该词是否存在于哈希表中
                String word = entry.getKey();
                if (!invertedIndex.containsKey(word)) {
                    // 不包含，就创建该词
                    invertedIndex.put(word, new ArrayList<>());
                }
                // 运行到这里，说明词一定存在了。接下来就是计算权重并统计
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                weight.setWeight(entry.getValue().titleCnt * 10 + entry.getValue().contentCnt);
                invertedIndex.get(word).add(weight);
            }
        }
    }

    // 新增正排索引
    private DocInfo buildForward(String title, String url, String content) {
        // 1、检验参数是否合法
        if (!StringUtils.hasText(title)
                || !StringUtils.hasText(url)
                || !StringUtils.hasText(content)) {
            System.out.println("文档标题或文档url或文档正文为空");
            return null;
        }
        // 2、构建DocInfo对象
        DocInfo docInfo = new DocInfo();
        // 文档Id和数组下标是相对应的
        // 初始时，数组长度为0，而文档Id也是0，因此docId=数组长度
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        // 对于共享资源的访问进行加锁
        synchronized (LOCKER1) {
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    /**
     * 4、将索引保存到本地磁盘中
     * 这里使用的是 Jackson库 将对象转换为JSON数据保存到文件中
     */
    public void save() {
        System.out.println("开始保存索引");
        long begin = System.currentTimeMillis();
        // 先判断目录是否存在
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            // 如果不存在，就创建目录
            indexPathFile.mkdirs();
        }
        try {
            // 将正排索引和倒排索引分别保存到对应文件中
            OBJECT_MAPPER.writeValue(new File( INDEX_PATH+ "forwardIndex.txt"), forwardIndex);
            OBJECT_MAPPER.writeValue(new File(INDEX_PATH + "invertedIndex.txt"), invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("索引保存完成, 耗时: "+(end-begin)+" ms");
    }

    /**
     * 5、将本地磁盘中的索引加载的内存中
     */
    public void load() {
        System.out.println("开始加载索引");
        long begin = System.currentTimeMillis();
        try {
            // 将文件中保存的正排索引结构和倒排索引结构分别加载到内存中
            forwardIndex = OBJECT_MAPPER.readValue(new File(INDEX_PATH + "forwardIndex.txt"),
                    new TypeReference<List<DocInfo>>() {});
            invertedIndex = OBJECT_MAPPER.readValue(new File(INDEX_PATH + "invertedIndex.txt"),
                    new TypeReference<HashMap<String, List<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("索引加载完成, 耗时: "+(end-begin)+" ms");
    }

    public static void main(String[] args) {
        // 验证加载索引是否正确(端点调试)
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成");
    }
}
